/*
 * Created by IntelliJ IDEA.
 * User: mark
 * Date: 5/02/2004
 * Time: 17:17:45
 */
package com.talios.jira.idea;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.source.tree.ChameleonElement;
import com.intellij.psi.javadoc.PsiDocTag;
import com.intellij.util.IncorrectOperationException;
import com.talios.jira.browser.JiraItem;

/**
 * @jira INTPLUG-39 Classes should also get annotated when working on an issue
 * @jira INTPLUG-91 Creating classes and methods gives NullPointer when working on issue
 * @jira INTPLUG-129 infinite additions of @jira tag hung up IDEA
 */
public class JiraAnnotationListener implements DocumentListener {
    private static final Logger LOGGER = Logger.getInstance(JiraAnnotationListener.class.toString());

    private JiraConfigurationComponent jiraConfigurationComponent;
    private JiraConfiguration configuration;
    private Project project;

    public JiraAnnotationListener(JiraConfigurationComponent jiraConfigurationComponent) {
        this.jiraConfigurationComponent = jiraConfigurationComponent;
        configuration = jiraConfigurationComponent.getConfiguration();
    }

    public void beforeDocumentChange(DocumentEvent documentEvent) {

    }

    /**
     * @param documentEvent
     * @jira INTPLUG-27 Annotated javadoc is not automatically aligned to methods
     * @jira INTPLUG-66 Need an option to disable annotation
     * @jira INTPLUG-39 Classes should also get annotated when working on an issue
     * @jira INTPLUG-129 infinite additions of @jira tag hung up IDEA
     */
    public void documentChanged(final DocumentEvent documentEvent) {
        project = jiraConfigurationComponent.getProject();

        if (jiraConfigurationComponent.getItemWorkedOn() == null) return;

        FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
        final FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
        final PsiManager psiManager = PsiManager.getInstance(project);

        Document selectedDocument = documentEvent.getDocument();
        VirtualFile selectedFile = fileDocumentManager.getFile(selectedDocument);

        PsiFile psiFile = psiManager.findFile(selectedFile);

        // We only annotate java code, and only if annotations are enable
        if (configuration.getAnnotateIssues()) {
            final PsiJavaFile javaFile = (PsiJavaFile) psiFile;

            /**
             * @jira INTPLUG-39 Classes should also get annotated when working on an issue
             * @jira INTPLUG-39 Classes should also get annotated when working on an issue
             */
            ApplicationManager.getApplication().invokeLater(new Runnable() {
                /**
                 * @jira INTPLUG-31 If method already has javadoc, append annotation
                 * @jira INTPLUG-38 Modifying code outside of a method/class throws NullPointerExceptions
                 * @jira INTPLUG-39 Classes should also get annotated when working on an issue
                 */
                public void run() {

                    int offset = fileEditorManager.getSelectedTextEditor().getCaretModel().getOffset();
                    PsiElement context;

                    // Annotate the method
                    context = javaFile.findElementAt(offset);
                    if (context != null) {
                        PsiElement methodToAnnotate = PsiUtils.findParentPsiElement(context, PsiMethod.class);
                        boolean contextInComment = PsiUtils.findParentPsiElement(context, PsiComment.class) != null;

                        if (!contextInComment && methodToAnnotate != null) {
                            annotatePsiElement(methodToAnnotate, documentEvent);
                        }
                    }

                    // Annotate the class
                    context = javaFile.findElementAt(offset);
                    if (context != null) {
                        PsiElement classToAnnotate = PsiUtils.findParentPsiElement(context, PsiClass.class);
                        boolean contextInComment = PsiUtils.findParentPsiElement(context, PsiComment.class) != null;

                        if (!contextInComment && classToAnnotate != null) {
                            annotatePsiElement(classToAnnotate, documentEvent);
                        }
                    }

                }

            });

        }
    }

    /**
     * @jira INTPLUG-39 Classes should also get annotated when working on an issue
     */
    private void annotatePsiElement(final PsiElement element, final DocumentEvent documentEvent) {

        if (element == null) {
            return;
        }

        final PsiElement comment = element.getFirstChild();

        CommandProcessor.getInstance().executeCommand(project, new Runnable() {
            /**
             * @jira INTPLUG-39 Classes should also get annotated when working on an issue
             */
            public void run() {
                ApplicationManager.getApplication().runWriteAction(new Runnable() {
                    /**
                     * @jira INTPLUG-27 Annotated javadoc is not automatically aligned to methods
                     * @jira INTPLUG-31 If element already has javadoc, append annotation
                     * @jira INTPLUG-32 Annotated javadoc should put issue description next to @jira
                     * @jira INTPLUG-37 Editing element signature of interface doesn't get annontated
                     */
                    public void run() {
                        PsiDocumentManager pdm = PsiDocumentManager.getInstance(project);
                        // TODO: Add check for element.isValid() - if not valid, find valid token to insert
                        // comment into.
                        pdm.commitDocument(documentEvent.getDocument());
                        try {
                            PsiElement added = null;

                            if (comment != null && comment instanceof PsiComment) {
                                boolean containsIssueDoc = containsIssueDoc((PsiComment) comment, jiraConfigurationComponent.getItemWorkedOn());

                                if (!containsIssueDoc) {
                                    PsiComment newElement = getJavaDocForIssue((PsiComment) comment, jiraConfigurationComponent.getItemWorkedOn());
                                    if (newElement != null) {
                                        added = comment.replace(newElement);
                                    }
                                }
                            } else {
                                PsiComment newElement = getJavaDocForIssue(jiraConfigurationComponent.getItemWorkedOn());
                                // Possibly editing an interface where theres no body.
                                if (newElement != null) {
                                    added = element.addBefore(newElement, comment);
                                }
                            }

                            if (added != null) {
                                final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
                                codeStyleManager.reformat(added);
                            }

                        } catch (IncorrectOperationException e) {
                            LOGGER.error("Unable to add the javadoc comment.", e);
                        } finally {
                            // make sure PSI is in sync with a certain document
                            pdm.commitDocument(documentEvent.getDocument());
                        }
                    }
                });
            }

        }, "Comment", null);
    }

    /**
     * Check's if the provided PsiComment element alredy contains a reference to the desired Jira issue.
     *
     * @jira INTPLUG-31 If method already has javadoc, append annotation
     * @jira INTPLUG-91 Creating classes and methods gives NullPointer when working on issue
     */
    private boolean containsIssueDoc(PsiComment comment, JiraItem itemWorkedOn) {
        if (comment != null && !(comment instanceof ChameleonElement)) {
            try {
                PsiElement[] children = comment.getChildren();
                for (PsiElement child : children) {
                    if (child instanceof PsiDocTag && child.getText().startsWith("@jira")) {
                        if (child.getText().indexOf(itemWorkedOn.getKey()) != -1) {
                            return true;
                        }
                    }
                }
            } catch (NullPointerException e) {
                return false;
            }
        }
        return false;
    }

    /**
     * @jira INTPLUG-31 If method already has javadoc, append annotation
     */
    private String getIssueCommentLine(JiraItem jiraItem) {
        StringBuffer comment = new StringBuffer();
        comment.append(" * @jira ");
        comment.append(jiraItem.getKey());
        comment.append(jiraItem.getTitle());
        comment.append('\n');
        return comment.toString();
    }

    /**
     * Return a new virgin Javadoc element for the selected JiraItem
     *
     * @param jiraItem
     * @jira INTPLUG-31 If method already has javadoc, append annotation
     */
    private PsiComment getJavaDocForIssue(JiraItem jiraItem) throws IncorrectOperationException {
        PsiManager psiManager = PsiManager.getInstance(project);
        PsiElementFactory elementFactory = psiManager.getElementFactory();

        String commentString;

        StringBuffer comment = new StringBuffer();
        comment.append("/**\n");
        comment.append(getIssueCommentLine(jiraItem));
        comment.append(" */");

        commentString = comment.toString();

        PsiComment newElement = elementFactory.createCommentFromText(commentString, null);
        return newElement;
    }

    /**
     * Returns a modified Javadoc element for the selected JiraItem based on an orriginal Javadoc element.
     *
     * @jira INTPLUG-31 If method already has javadoc, append annotation
     */
    private PsiComment getJavaDocForIssue(PsiComment originalComment, JiraItem jiraItem) throws IncorrectOperationException {
        PsiManager psiManager = PsiManager.getInstance(project);
        PsiElementFactory elementFactory = psiManager.getElementFactory();
        LOGGER.info(originalComment.getText());
        String[] commentLines = originalComment.getText().split("\n");
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < commentLines.length; i++) {
            String commentLine = commentLines[i];

            // last line, append our new comment entry
            if (i == commentLines.length - 1) {
                buf.append(getIssueCommentLine(jiraItem));
                buf.append(commentLine);
            } else {
                buf.append(commentLine);
                buf.append('\n');
            }
        }
        String commentString = buf.toString();

        PsiComment newElement = elementFactory.createCommentFromText(commentString, null);
        return newElement;
    }

}
